<?php
/* 
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

class session extends singleton //implements session_interface
{

	protected $driver = 'file';
	protected $save_path = '';
	protected $hash = 'md5';
	protected $expiration = 7200;
	protected $match_ip = false;
	protected $match_useragent = true;
	protected $cookie_name	= 'sessionid';
	protected $cookie_prefix = '';
	protected $cookie_path = '';
	protected $cookie_domain = '';
	protected $update_time = 300;
	protected $time_reference = 'time';
	protected $gc_probability = 5;
	
	protected $sess_data = array();
	protected $sess_handler = null;
	protected $sess_started = false;
	protected $now = null;

	static public function &instance() {
		return parent::instance(__CLASS__);
	}

	protected function __construct() {}

	public function __set($key, $value = null) {
		$this->set_data($key, $value);
	}

	public function &__get($key) {
		return $this->data($key);
	}

	public function __isset($key) {
		return $this->isset_data($key);
	}

	public function __unset($key) {
		$this->unset_data($key);
	}
	
	public function sid() {

		if(is_array($this->sess_data) && isset($this->sess_data['session_id']))
			return $this->sess_data['session_id'];

		return false;
	}

	public function &data($key = null, $default = null) {
		if(is_array($this->sess_data) && isset($this->sess_data['user_data'])) {

			if(is_null($key)){
				return $this->sess_data['user_data'];
			}
			else if(isset($this->sess_data['user_data'][$key]))
				return $this->sess_data['user_data'][$key];
		}
		
		return $default;
	}
	
	public function set_data($key, $value = null) {

		if(is_array($this->sess_data) && isset($this->sess_data['user_data'])) {

			if(!is_null($value))
				$this->sess_data['user_data'][$key] = $value;
			else
				return $this->unset_data($key);

			$this->sess_write();
			
			return true;
		}

		return false;
	}
	
	public function isset_data($key) {
		return !is_null($this->data($key));
	}

	public function unset_data($key) {

		if(is_array($this->sess_data) && 
		   isset($this->sess_data['user_data'], $this->sess_data['user_data'][$key])) {

			unset($this->sess_data['user_data'][$key]);
			
			$this->sess_write();

			return true;
		}

		return false;
	}

	public function start() {
		
		if(!$this->sess_started) {
			$this->sess_start();
			$this->sess_started = true;
		}
		
		return $this->sess_started;
	}

	public function expire() {
		$sess_id = $this->sid();
		
		if($this->sess_started && $sess_id) {
			$this->sess_started = false;
			return $this->sess_handler->destroy($sess_id);
		}
		
		return false;
	}

	protected function sess_start() {

		if (bc::enabled()) bc::info('session start');

		$conf = config::instance()->session;

		// Set the "now" time.  Can either be GMT or server time, based on the
		// config prefs.  We use this to set the "last activity" time
		$this->now = $this->get_time();

		// process config
		$atts = array(
			'driver', 'save_path', 'expiration',
			'match_ip', 'match_useragent',
			'cookie_name', 'cookie_prefix', 'cookie_path', 'cookie_domain',
			'update_time', 'time_reference', 'gc_probability', 'hash'
		);

		$this->cookie_domain = config::instance()->cookie__domain;
		$this->cookie_path = config::instance()->cookie__path;

		foreach($atts as $key) {		
			if(isset($conf[$key]) && !is_null($conf[$key]))
				$this->$key = $conf[$key];
		}

		// Set the session length. If the session expiration is
		// set to zero we'll set the expiration two years from now.
		if ($this->expiration == 0)
		{
			$this->expiration = 60*60*24*365*2;
		}

		// Set the cookie name
		$this->cookie_name = $this->cookie_prefix . $this->cookie_name;

		if(empty($this->driver))
			$this->driver = 'file';

		if(empty($this->hash))
			$this->hash = 'md5';

		$handler = 'session_' . $this->driver;
		$this->sess_handler = new $handler($this->save_path);

		// create a new one.  If it does, we'll update it.	
		if(!$this->sess_read()) {
			$this->sess_create();
		} else {
			if (bc::enabled()) bc::info($this->sid(), 'session id');
			if (bc::enabled()) bc::log($this->sess_data, 'session data');
			$this->sess_update();
		}

		// delete expired sessions
		$this->sess_gc();
		
	}

	protected function sess_create() {

		$sess_id = $this->sess_generate_id();

		$this->sess_data = array(
			'session_id' => $sess_id,
			'ip_address' => request::instance()->get_ip(),
			'user_agent' => substr(request::instance()->get_user_agent(), 0, 50),
			'last_activity'	=> $this->now,
			'user_data' => array()
		);

		$this->sess_handler->create($this->sess_data);

		if (bc::enabled()) bc::info($this->sid(), 'new session id');

		// Write the cookie
		$this->sess_cookie();
	}

	protected function sess_read() {

		$sess_id = request::instance()->cookie($this->cookie_name);

		// no session cookie? goodbye cruel world...
		if(is_null($sess_id))
			return false;

		$session = $this->sess_handler->read($sess_id);

		// Is the session data we unserialized an array with the correct format?
		if(!is_array($session) || !isset($session['session_id'], $session['ip_address'], $session['user_agent'], $session['last_activity'], $session['user_data']))
		{
			$this->sess_handler->destroy($sess_id);
			return false;
		}

		// Is the session current?
		if (($session['last_activity'] + $this->expiration) < $this->now)
		{
			$this->sess_handler->destroy($sess_id);
			return false;
		}

		// Does the IP Match?
		if ($this->match_ip && $session['ip_address'] != request::instance()->get_ip())
		{
			$this->sess_handler->destroy($sess_id);
			return false;
		}

		// Does the User Agent Match?
		if ($this->match_useragent && trim($session['user_agent']) != trim(substr(request::instance()->get_user_agent(), 0, 50)))
		{
			$this->sess_handler->destroy($sess_id);
			return false;
		}

		// Session is valid!
		$this->sess_data = $session;

		return true;
	}

	protected function sess_write() {
		$this->sess_data['last_activity'] = $this->now;
		$r = $this->sess_handler->write($this->sess_data);
	}

	protected function sess_update() {

		// We only update the session every five minutes by default
		if (($this->sess_data['last_activity'] + $this->update_time) >= $this->now)
		{
			return;
		}

		$data = $this->sess_data['user_data'];

		$this->sess_create();
		
		$this->sess_data['user_data'] = $data;

		$this->sess_write();
	}

	protected function sess_generate_id() {
		
		$new_sessid = '';

		while (strlen($new_sessid) < 32) {
			$new_sessid .= mt_rand(0, mt_getrandmax());
		}

		// To make the session ID even more secure we'll combine it with the user's IP
		$new_sessid .= request::instance()->get_ip();

		// Turn it into a hash
		$new_sessid = str_replace(array('/', '='), array('-', ''), base64_encode(hash($this->hash, uniqid($new_sessid, true), true)));

		return $new_sessid;
	}

	protected function sess_cookie() {

		response::instance()->cookie(
				array(
					'name' => $this->cookie_name,
					'value' => $this->sid(),
					'expire' => $this->expiration + time(),
					'path' => $this->cookie_path,
					'domain' => $this->cookie_domain,
					'httponly' => 0
				)
			);
	}

	protected function sess_gc() {

		if ((rand() % 100) < $this->gc_probability)
		{
			$expire = $this->now - $this->expiration;

			$num_expired = $this->sess_handler->gc($expire);

			if (bc::enabled()) bc::info(sprintf('%d expired sessions', $num_expired), 'session.gc');
		}
	}

	protected function get_time() {
		
		if (strtolower($this->time_reference) == 'gmt')
		{
			$now = time();
			$time = mktime(gmdate("H", $now), gmdate("i", $now), gmdate("s", $now), gmdate("m", $now), gmdate("d", $now), gmdate("Y", $now));
		}
		else
		{
			$time = time();
		}

		return $time;
	}

}

